package com.hucsmn.examsystem.common;

import com.hucsmn.examsystem.util.BuildStrings;

import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 选择题。
 */
public class Problem implements Serializable, Cloneable {
    /**
     * 最小选项数。
     */
    public static final int MIN_CHOICES = 2;
    /**
     * 最大选项数。
     */
    public static final int MAX_CHOICES = 26;
    private static final long serialVersionUID = -8881279952181443882L;

    // 编号、题目、是否多选、选项列表、题目分数。
    private int id;
    private String problem;
    private boolean multiple;
    private Choice[] choices;
    private int score;

    public Problem(int id, String problem, int score, String... choices) {
        this(id, problem, score, false, choices);
    }

    public Problem(int id, String problem, int score, boolean multiple, String... choices) {
        if (id < 0) {
            throw new IllegalArgumentException("negative problem id");
        }
        this.id = id;
        setProblem(problem);
        setChoices(choices);
        setMultiple(multiple);
        setScore(score);
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        if (id < 0) {
            throw new IllegalArgumentException("negative problem id");
        }
        this.id = id;
    }

    public String getProblem() {
        return problem;
    }

    public void setProblem(String problem) {
        if (problem == null) {
            throw new NullPointerException();
        }
        if (problem.isEmpty()) {
            throw new IllegalArgumentException("problem cannot be empty");
        }
        this.problem = problem;
    }

    public boolean isMultiple() {
        return multiple;
    }

    public void setMultiple(boolean multiple) {
        long toggled = Arrays.stream(choices).filter(Choice::isToggled).count();
        long correct = Arrays.stream(choices).filter(Choice::isAnswer).count();
        if (!multiple && (toggled > 1 || correct > 1)) {
            throw new IllegalStateException("cannot disable multiple choices");
        }
        this.multiple = multiple;
    }

    public int length() {
        return choices.length;
    }

    public void setChoices(String... choices) {
        if (choices == null) {
            throw new NullPointerException();
        }
        if (choices.length < MIN_CHOICES || choices.length > MAX_CHOICES) {
            throw new IllegalArgumentException("too few or too many choices");
        }

        int i = 0;
        Set<String> dedup = new HashSet<>(choices.length);
        Choice[] newChoices = new Choice[choices.length];
        for (String choice : choices) {
            if (dedup.contains(choice)) {
                throw new IllegalArgumentException("duplicated choices");
            }
            dedup.add(choice);

            Choice c = new Choice(choice);
            newChoices[i] = c;
            i++;
        }
        this.choices = newChoices;
    }

    public void toggle(int i, boolean toggled) {
        if (i < 0 || i >= choices.length) {
            throw new ArrayIndexOutOfBoundsException(i);
        }
        choices[i].setToggled(toggled);
    }

    public void toggle(String choice, boolean toggled) {
        for (Choice c : choices) {
            if (c.getChoice().equals(choice)) {
                c.setToggled(toggled);
                return;
            }
        }
        throw new NoSuchElementException(choice);
    }

    public void clearToggled() {
        for (Choice choice : choices) {
            choice.setToggled(false);
        }
    }

    public int[] getToggled() {
        int n = 0;
        int[] idx = new int[choices.length];
        for (int i = 0; i < choices.length; i++) {
            if (!choices[i].isToggled()) {
                continue;
            }
            idx[n++] = i;
        }
        int[] ret = new int[n];
        System.arraycopy(idx, 0, ret, 0, n);
        return ret;
    }

    public void setToggled(int... toggled) {
        Arrays.sort(toggled);
        boolean[] bitmap = new boolean[choices.length];
        for (int i : toggled) {
            if (i < 0 || i >= bitmap.length) {
                throw new ArrayIndexOutOfBoundsException(i);
            }
            bitmap[i] = true;
        }
        for (int i = 0; i < bitmap.length; i++) {
            choices[i].setToggled(bitmap[i]);
        }
    }

    public int[] getAnswers() {
        int n = 0;
        int[] idx = new int[choices.length];
        for (int i = 0; i < choices.length; i++) {
            if (!choices[i].isAnswer()) {
                continue;
            }
            idx[n++] = i;
        }
        int[] ret = new int[n];
        System.arraycopy(idx, 0, ret, 0, n);
        return ret;
    }

    public void setAnswers(int... answers) {
        Arrays.sort(answers);
        boolean[] bitmap = new boolean[choices.length];
        for (int i : answers) {
            if (i < 0 || i >= bitmap.length) {
                throw new ArrayIndexOutOfBoundsException(i);
            }
            bitmap[i] = true;
        }
        for (int i = 0; i < bitmap.length; i++) {
            choices[i].setAnswer(bitmap[i]);
        }
    }

    public int getScore() {
        return score;
    }

    public void setScore(int score) {
        if (score <= 0) {
            throw new IllegalArgumentException("score cannot be non-positive");
        }
        this.score = score;
    }

    public boolean isCorrect() {
        for (Choice choice : choices) {
            if (choice.isToggled() != choice.isAnswer()) {
                return false;
            }
        }
        return true;
    }

    public int getFinalScore() {
        return isCorrect() ? score : 0;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Problem problem1 = (Problem) o;
        return multiple == problem1.multiple &&
                score == problem1.score &&
                problem.equals(problem1.problem) &&
                Arrays.equals(choices, problem1.choices);
    }

    @Override
    public int hashCode() {
        int result = Objects.hash(problem, multiple, score);
        result = 31 * result + Arrays.hashCode(choices);
        return result;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb
                .append(problem)
                .append(multiple ? "（多选题，" : "（单选题，")
                .append(score).append(" 分）：").append(BuildStrings.NEWLINE);

        char label = 'A';
        for (Choice choice : choices) {
            sb
                    .append(BuildStrings.INDENT).append(label).append(". ")
                    .append(choice.getChoice()).append(BuildStrings.NEWLINE);
            label++;
        }

        return sb.toString();
    }

    public String detailedToString() {
        final StringBuilder sb = new StringBuilder();
        sb
                .append(problem)
                .append(multiple ? "（多选题，" : "（单选题，")
                .append(score).append(" 分，得 ")
                .append(getFinalScore()).append(" 分）")
                .append(BuildStrings.NEWLINE);

        char label = 'A';
        for (Choice choice : choices) {
            sb
                    .append(BuildStrings.INDENT)
                    .append(label).append(". ")
                    .append(choice.getChoice()).append(BuildStrings.NEWLINE);
            label++;
        }

        String toggled = Arrays.stream(getToggled())
                .mapToObj(i -> String.valueOf((char) ('A' + i)))
                .collect(Collectors.joining(", "));
        sb
                .append(BuildStrings.INDENT)
                .append("勾选情况：").append(toggled)
                .append(BuildStrings.NEWLINE);

        String answers = Arrays.stream(getAnswers())
                .mapToObj(i -> String.valueOf((char) ('A' + i)))
                .collect(Collectors.joining(", "));
        sb
                .append(BuildStrings.INDENT)
                .append("正确答案：").append(answers)
                .append(BuildStrings.NEWLINE);

        return sb.toString();
    }

    @Override
    public Problem clone() throws CloneNotSupportedException {
        Problem problem = (Problem) super.clone();
        problem.setChoices(Arrays.stream(this.choices).map(Choice::getChoice).toArray(String[]::new));
        if (Arrays.stream(problem.choices).anyMatch(Objects::isNull)) {
            throw new CloneNotSupportedException();
        }
        problem.setAnswers(getAnswers());
        problem.setToggled(getToggled());
        return problem;
    }

    /**
     * 题目选项。
     */
    public static class Choice implements Serializable, Cloneable {
        private static final long serialVersionUID = 447426963037300513L;

        // 选项、是否选中、是否为正确答案（客户端只有在交卷后才可以获得）。
        private final String choice;
        private boolean toggled;
        private boolean answer;

        public Choice(String choice) {
            this(choice, false, false);
        }

        public Choice(String choice, boolean answer) {
            this(choice, answer, false);
        }

        public Choice(String choice, boolean answer, boolean toggled) {
            if (choice == null) {
                throw new NullPointerException();
            }
            if (choice.isEmpty()) {
                throw new IllegalArgumentException("choice cannot be empty");
            }
            this.choice = choice;

            setAnswer(answer);
            setToggled(toggled);
        }

        public String getChoice() {
            return choice;
        }

        public boolean isToggled() {
            return toggled;
        }

        public void setToggled(boolean toggled) {
            this.toggled = toggled;
        }

        public boolean isAnswer() {
            return answer;
        }

        public void setAnswer(boolean answer) {
            this.answer = answer;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Choice choice1 = (Choice) o;
            return choice.equals(choice1.choice);
        }

        @Override
        public int hashCode() {
            return Objects.hash(choice);
        }

        @Override
        public Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
}
